﻿using System;
using System.Linq;
using System.Linq.Expressions;

namespace MathService.Extentions.Expressions
{
    public static class ExpressionExtentions
    {
        private class SubstExpressionVisitor : ExpressionVisitorEx
        {
            public ParameterExpression ParamExpressionToSubstitute { private get; set; }

            public LambdaExpression SubstExpression { private get; set; }

            public new Expression Visit(Expression exp)
            {
                return base.Visit(exp);
            }

            protected override Expression VisitUnary(UnaryExpression node)
            {
                return node.Operand == ParamExpressionToSubstitute
                    ? Expression.MakeUnary(node.NodeType, SubstExpression.Body, node.Type)
                    : base.VisitUnary(node);
            }

            protected override Expression VisitMethodCall(MethodCallExpression node)
            {
                var lv_NumArgsToSubstituteAmongMethodArgs =
                    (from expr in node.Arguments
                     where (expr == ParamExpressionToSubstitute)
                     select expr).Count();
                if(lv_NumArgsToSubstituteAmongMethodArgs == 0)
                    return base.VisitMethodCall(node);
                var arguments = node.Arguments.Select(arg => arg == ParamExpressionToSubstitute ? SubstExpression.Body : Visit(arg)).ToList();
                return Expression.Call(node.Object, node.Method, arguments);
            }

            protected override Expression VisitBinary(BinaryExpression node)
            {
                Expression left, right;
                var lv_SubstLeft = false;
                var lv_SubstRight = false;

                if(node.Left == ParamExpressionToSubstitute)
                {
                    left = SubstExpression.Body;
                    lv_SubstLeft = true;
                }
                else
                    left = node.Left;

                if(node.Right == ParamExpressionToSubstitute)
                {
                    right = SubstExpression.Body;
                    lv_SubstRight = true;
                }
                else
                    right = node.Right;
                if(!lv_SubstLeft && !lv_SubstRight) return base.VisitBinary(node);
                if(!lv_SubstLeft)
                    left = Visit(left);
                if(!lv_SubstRight)
                    right = Visit(right);
                return Expression.MakeBinary(node.NodeType, left, right, node.IsLiftedToNull, node.Method);
            }
        }

        public static LambdaExpression Substitute(this LambdaExpression Expr, LambdaExpression Substitution)
        {
            var lv_Parameters = Expr.Parameters;
            var lv_sParameters = Substitution.Parameters;
            if(lv_sParameters.Count != 1)
                throw new FormatException("Количество аргументов подстановки не равно 1");
            var SubstituteParameter = lv_sParameters[0];
            if(!lv_Parameters.Contains(p => p.Name == SubstituteParameter.Name && p.Type == SubstituteParameter.Type))
                throw new FormatException("Во входном выражении отсутсвуте подставляемый параметр");

            var visitor = new SubstitutionVisitor(Substitution);
            var result = visitor.Visit(Expr);

            return result as LambdaExpression;
        }

        public static LambdaExpression
            Substitute<TDelegate>
            (
                this LambdaExpression MainExpression,
                string ParameterName,
                Expression<TDelegate> SubstExpression
            )
        {
            #region Проверка параметров

            var lv_MainParameter = (from parameter in MainExpression.Parameters
                                    where parameter.Name == ParameterName
                                    select parameter).FirstOrDefault();

            if(lv_MainParameter == null)
            {
                throw new Exception
                    (
                    string.Format("Could not find input parameter \"{0}\" in expression \"{1}\"",
                        ParameterName,
                        MainExpression)
                    );
            }

            var lv_SubstitutionParameter = (from parameter in SubstExpression.Parameters
                                            where parameter.Name == ParameterName
                                            select parameter).FirstOrDefault();

            if(lv_SubstitutionParameter == null)
            {
                throw new Exception
                    (
                    string.Format("Could not find substitution parameter \"{0}\" in expression \"{1}\"",
                        ParameterName,
                        SubstExpression)
                    );
            }

            if(lv_SubstitutionParameter.Type != lv_MainParameter.Type)
            {
                throw new Exception
                    (
                    string.Format
                        (
                            "The substitute expression return type \"{0}\" does not match the type of the substituted variable \"{1}:{2}\"",
                            SubstExpression.Type,
                            ParameterName,
                            lv_MainParameter.Type
                        )
                    );
            }

            #endregion


            //var pars = new List<ParameterExpression>();

            //pars.AddRange(MainExpression.Parameters);
            var pars = MainExpression.Parameters.ToList();

            var lv_IdxToSubst = pars.IndexOf(lv_MainParameter);

            pars.RemoveAt(lv_IdxToSubst);

            foreach(var lv_SubstPe in SubstExpression.Parameters)
            {
                var lv_NumParamsOfTheSameNameInMainExpression =
                    (from pe in pars
                     where pe.Name == lv_SubstPe.Name
                     select pe).Count();
                if(lv_NumParamsOfTheSameNameInMainExpression != 0)
                    throw new Exception
                        (
                            string.Format
                                (
                                    "Input parameter of name \"{0}\" already exists in the main expression",
                                    lv_SubstPe.Name
                                )
                        );
                pars.Insert(lv_IdxToSubst, lv_SubstPe);
                lv_IdxToSubst++;
                //continue;
                /*
                                throw new Exception
                                (
                                    string.Format
                                    (
                                        "Input parameter of name \"{0}\" already exists in the main expression",
                                        substPe.Name
                                    )
                                );
                */
            }

            var modifiedMain =
                Expression.Lambda
                (
                    MainExpression.Body,
                    pars.ToArray()
                );

            var visitor =
                new SubstExpressionVisitor
                {
                    SubstExpression = SubstExpression,
                    ParamExpressionToSubstitute = lv_MainParameter
                };

            var substResult = (LambdaExpression)visitor.Visit(modifiedMain);

            return substResult;
        }
    }
}
